/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.rcs.folders;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.rcs.servlets.exceptions.InvalidCacheHeaderException;
import org.rcs.servlets.exceptions.InvalidRangeException;
import org.rcs.servlets.exceptions.InvalidResumeHeaderException;
import org.rcs.servlets.utils.FileUtils;
import org.rcs.servlets.utils.Range;

/**
 *
 * @author ruben
 */
public abstract class AbstractItem implements VirtualItem {

  private static final int DEFAULT_BUFFER_SIZE = 10240; // ..bytes = 10KB.
  private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";
 
  
  @Override
  public String getDisplayName() {
    return getName();
  }

  @Override
  public Date getModifiedDate() {
    return new Date();
  }

  @Override
  public Date getExpiresDate() {
    return new Date();
  }

  @Override
  public String getETag() {
    return getName() + "_" + getContentLength() + "_" + getModifiedDate().getTime();
  }

  @Override
  public String getContentType() {
    return null ;
  }

  @Override
  public void sendContent(HttpServletResponse response, HttpServletRequest request, String requestedPath, boolean onlyHeader) throws Exception {
    String eTag = getETag();
    long length = getContentLength();
    String fileName = getDisplayName() ;
    
    try {
      long lastModified = getModifiedDate().getTime();
    
      validateHeadersForCaching(request, eTag, lastModified);
      validateHeadersForResume(request, eTag, lastModified);

      // Validate and process range -------------------------------------------------------------
      // Prepare some variables. The full Range represents the complete file.
      Range full = new Range(0, length - 1, length);
      List<Range> ranges = calcRangeList(request, full, lastModified, eTag);

      // Prepare and initialize response --------------------------------------------------------
      // Get content type by file name and set default GZIP support and content disposition.
      String contentType = prepareAndInitializeResponse(response, request, fileName, eTag, lastModified);
      boolean acceptsGzip = acceptsGZip(request, contentType);

      sendRequestedFile(response, contentType, acceptsGzip, !onlyHeader, ranges, full);
    } catch (InvalidCacheHeaderException ie) {
      response.setHeader("ETag", eTag); // Required in 304.
      response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
    } catch (InvalidResumeHeaderException irhe) {
      response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
    } catch (InvalidRangeException ex) {
      response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
      response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
    }
  }

  protected abstract void sendContentRange(OutputStream output, long start, long length) throws IOException ;
    
  
  /**
   * Returns true if the given accept header accepts the given value.
   *
   * @param acceptHeader The accept header.
   * @param toAccept The value to be accepted.
   * @return True if the given accept header accepts the given value.
   */
  private static boolean accepts(String acceptHeader, String toAccept) {
    String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");
    Arrays.sort(acceptValues);
    return Arrays.binarySearch(acceptValues, toAccept) > -1
            || Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1
            || Arrays.binarySearch(acceptValues, "*/*") > -1;
  }

  /**
   * Returns true if the given match header matches the given value.
   *
   * @param matchHeader The match header.
   * @param toMatch The value to be matched.
   * @return True if the given match header matches the given value.
   */
  private static boolean matches(String matchHeader, String toMatch) {
    String[] matchValues = matchHeader.split("\\s*,\\s*");
    Arrays.sort(matchValues);
    return Arrays.binarySearch(matchValues, toMatch) > -1
            || Arrays.binarySearch(matchValues, "*") > -1;
  }

  //////////////////////////////
  private void addIfRange(List<Range> ranges, HttpServletRequest request, Range full, long lastModified, String eTag) throws IOException {
    // If-Range header should either match ETag or be greater then LastModified. If not,
    // then return full file.
    String ifRange = request.getHeader("If-Range");

    if (ifRange != null && !ifRange.equals(eTag))
      try {
        long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.
        if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified)
          ranges.add(full);
      } catch (IllegalArgumentException ignore) {
        ranges.add(full);
      }
  }

  protected List<Range> calcRangeList(HttpServletRequest request, Range full, long lastModified, String eTag) throws IOException, InvalidRangeException {
    // Prepare some variables. The full Range represents the complete file.
    long length = full.getLength();
    List<Range> ranges = new ArrayList<Range>();

    // Validate and process Range and If-Range headers.
    String range = request.getHeader("Range");
    if (range != null) {

      // Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
      if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$"))
        throw new InvalidRangeException("Invalid range header");

      addIfRange(ranges, request, full, lastModified, eTag);

      // If any valid If-Range header, then process each part of byte range.
      if (ranges.isEmpty())
        for (String part : range.substring(6).split(","))
          ranges.add(Range.createFrom(part, length));
    }

    return ranges;
  }

  //////////////////////////////
  protected void validateHeadersForCaching(HttpServletRequest request, String eTag, long lastModified) throws InvalidCacheHeaderException {
    // If-None-Match header should contain "*" or ETag. If so, then return 304.
    String ifNoneMatch = request.getHeader("If-None-Match");

    if (ifNoneMatch != null && matches(ifNoneMatch, eTag))
      throw new InvalidCacheHeaderException(String.format("If-None-Match:%s", ifNoneMatch));

    // If-Modified-Since header should be greater than LastModified. If so, then return 304.
    // This header is ignored if any If-None-Match header is specified.
    long ifModifiedSince = request.getDateHeader("If-Modified-Since");

    if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified)
      throw new InvalidCacheHeaderException(String.format("If-Modified-Since:%s", request.getHeader("If-Modified-Since")));
  }

  protected void validateHeadersForResume(HttpServletRequest request, String eTag, long lastModified) throws InvalidResumeHeaderException {
    // If-Match header should contain "*" or ETag. If not, then return 412.
    String ifMatch = request.getHeader("If-Match");

    if (ifMatch != null && !matches(ifMatch, eTag))
      throw new InvalidResumeHeaderException(request.getHeader("If-Match"));

    // If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
    long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");

    if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified)
      throw new InvalidResumeHeaderException(request.getHeader("If-Unmodified-Since"));
  }

  protected boolean acceptsGZip(HttpServletRequest request, String contentType) {
    boolean result = false;

    if (contentType.startsWith("text")) {
      String acceptEncoding = request.getHeader("Accept-Encoding");
      result = acceptEncoding != null && accepts(acceptEncoding, "gzip");
    }

    return result;
  }

  protected String prepareAndInitializeResponse(HttpServletResponse response, HttpServletRequest request, String fileName, String eTag, long lastModified) {
    // Get content type by file name and content disposition.
    String contentType = getContentType() ;
    String disposition = "inline";

    // If content type is unknown, then set the default value.
    // For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
    // To add new content types, add new mime-mapping entry in web.xml.
    if (contentType == null)
      contentType = "application/octet-stream";

    // If content type is text, then determine whether GZIP content encoding is supported by
    // the browser and expand content type with the one and right character encoding.
    if (contentType.startsWith("text")) {
      String acceptEncoding = request.getHeader("Accept-Encoding");
      contentType += ";charset=UTF-8";
    } // Else, expect for images, determine content disposition. If content type is supported by
    // the browser, then set to inline, else attachment which will pop a 'save as' dialogue.
    else if (!contentType.startsWith("image")) {
      String accept = request.getHeader("Accept");
      disposition = accept != null && accepts(accept, contentType) ? "inline" : "attachment";
    }

    // Initialize response.
    response.reset();
    response.setBufferSize(DEFAULT_BUFFER_SIZE);
    response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");
    response.setHeader("Accept-Ranges", "bytes");
    response.setHeader("ETag", eTag);
    response.setDateHeader("Last-Modified", lastModified);
    response.setDateHeader("Expires", getExpiresDate().getTime());

    return contentType;
  }

  protected void sendRequestedFile(
          HttpServletResponse response, String contentType, boolean acceptsGzip, boolean content, List<Range> ranges, Range full) throws IOException {
    OutputStream output = null;

    try {
      // Open streams.
      output = response.getOutputStream();

      if (ranges.isEmpty() || ranges.get(0) == full)
        // Return full file.
        sendFullFile(response, full, contentType, content, acceptsGzip, output);
      else if (ranges.size() == 1)
        // Return single part of file.
        sendSinglePart(response, ranges.get(0), contentType, content, output);
      else
        // Return multiple parts of file.
        sendMultiPart(response, ranges, contentType, content, output);
    } finally {
      // Gently close streams.
      FileUtils.close(output);
    }
  }

  protected void sendFullFile(
          HttpServletResponse response, Range r, String contentType, boolean content, boolean acceptsGzip, OutputStream output) throws IOException {
    // Return full file.
    response.setContentType(contentType);
    response.setHeader("Content-Range", "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
    response.setStatus(HttpServletResponse.SC_OK) ;
            
    if (content) {
      if (acceptsGzip) {
        // The browser accepts GZIP, so GZIP the content.
        response.setHeader("Content-Encoding", "gzip");
        output = new GZIPOutputStream(output, DEFAULT_BUFFER_SIZE);
      } else
        // Content length is not directly predictable in case of GZIP.
        // So only add it if there is no means of GZIP, else browser will hang.
        response.setHeader("Content-Length", String.valueOf(r.getLength()));

      sendContentRange(output, r.getStart(), r.getLength());
    }
  }

  protected void sendSinglePart(HttpServletResponse response, Range r, String contentType, boolean content, OutputStream output) throws IOException {
    response.setContentType(contentType);
    response.setHeader("Content-Range", "bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());
    response.setHeader("Content-Length", String.valueOf(r.getLength()));
    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

    if (content)
      sendContentRange(output, r.getStart(), r.getLength());
  }

  protected void sendMultiPart(HttpServletResponse response, List<Range> ranges, String contentType, boolean content, OutputStream output) throws IOException {
    response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);
    response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

    if (content) {
      // Cast back to ServletOutputStream to get the easy println methods.
      ServletOutputStream sos = (ServletOutputStream) output;

      // Copy multi part range.
      for (Range r : ranges) {
        // Add multipart boundary and header fields for every range.
        sos.println();
        sos.println("--" + MULTIPART_BOUNDARY);
        sos.println("Content-Type: " + contentType);
        sos.println("Content-Range: bytes " + r.getStart() + "-" + r.getEnd() + "/" + r.getTotal());

        // Copy single part range of multi part range.
        sendContentRange(output, r.getStart(), r.getLength());
      }

      // End with multipart boundary.
      sos.println();
      sos.println("--" + MULTIPART_BOUNDARY + "--");
    }
  }
}
